BusyBird
view release on metacpan or search on metacpan
lib/BusyBird/StatusStorage/Memory.pm view on Meta::CPAN
package BusyBird::StatusStorage::Memory;
use v5.8.0;
use strict;
use warnings;
use parent ('BusyBird::StatusStorage');
use BusyBird::Util qw(set_param sort_statuses);
use BusyBird::Log qw(bblog);
use BusyBird::StatusStorage::Common qw(contains ack_statuses get_unacked_counts);
use BusyBird::DateTime::Format;
use Storable qw(dclone);
use Carp;
use List::Util qw(min);
use JSON;
use Try::Tiny;
sub new {
my ($class, %options) = @_;
my $self = bless {
timelines => {}, ## timelines should always be sorted.
}, $class;
$self->set_param(\%options, 'max_status_num', 2000);
if($self->{max_status_num} <= 0) {
croak "max_status_num option must be bigger than 0.";
}
return $self;
}
sub _log {
my ($self, $level, $msg) = @_;
bblog($level, $msg);
}
sub _index {
my ($self, $timeline, $id) = @_;
return -1 if not defined($self->{timelines}{$timeline});
my $tl = $self->{timelines}{$timeline};
my @ret = grep { $tl->[$_]{id} eq $id } 0..$#$tl;
confess "multiple IDs in timeline $timeline." if int(@ret) >= 2;
return int(@ret) == 0 ? -1 : $ret[0];
}
sub _acked {
my ($self, $status) = @_;
no autovivification;
return $status->{busybird}{acked_at};
}
sub save {
my ($self, $filepath) = @_;
if(not defined($filepath)) {
croak '$filepath is not specified.';
}
my $file;
if(!open $file, ">", $filepath) {
$self->_log("error", "Cannot open $filepath to write.");
return 0;
}
my $success;
try {
print $file encode_json($self->{timelines});
$success = 1;
}catch {
my $e = shift;
$self->_log("error", "Error while saving: $e");
$success = 0;
};
close $file;
return $success;
}
sub load {
my ($self, $filepath) = @_;
if(not defined($filepath)) {
croak '$filepath is not specified.';
}
my $file;
if(!open $file, "<", $filepath) {
$self->_log("notice", "Cannot open $filepath to read");
return 0;
}
my $success;
try {
my $text = do { local $/; <$file> };
$self->{timelines} = decode_json($text);
$success = 1;
}catch {
my $e = shift;
$self->_log("error", "Error while loading: $e");
$success = 0;
};
close $file;
return $success;
}
sub _is_timestamp_format_ok {
my ($timestamp_str) = @_;
return 1 if not defined $timestamp_str;
## It is very inefficient to parse $timestamp_str to check its
## format, because creating a DateTime object takes long time. We
## do it because BB::SS::Memory is just a reference
## implementation.
return defined(BusyBird::DateTime::Format->parse_datetime($timestamp_str));
}
sub put_statuses {
my ($self, %args) = @_;
croak 'timeline arg is mandatory' if not defined $args{timeline};
my $timeline = $args{timeline};
if(!defined($args{mode}) ||
($args{mode} ne 'insert'
&& $args{mode} ne 'update' && $args{mode} ne 'upsert')) {
croak 'mode arg must be insert/update/upsert';
}
my $mode = $args{mode};
my $statuses;
if(!defined($args{statuses})) {
croak 'statuses arg is mandatory';
}elsif(ref($args{statuses}) eq 'HASH') {
$statuses = [ $args{statuses} ];
}elsif(ref($args{statuses}) eq 'ARRAY') {
$statuses = $args{statuses};
}else {
croak 'statuses arg must be STATUS/ARRAYREF_OF_STATUSES';
}
foreach my $s (@$statuses) {
no autovivification;
croak "{id} field is mandatory in statuses" if not defined $s->{id};
croak "{busybird} field must be a hash-ref if present" if defined($s->{busybird}) && ref($s->{busybird}) ne "HASH";
croak "{created_at} field must be parsable by BusyBird::DateTime::Format" if !_is_timestamp_format_ok($s->{created_at});
my $acked_at = $s->{busybird}{acked_at}; ## avoid autovivification
croak "{busybird}{acked_at} field must be parsable by BusyBird::DateTime::Format" if !_is_timestamp_format_ok($acked_at);
}
my $put_count = 0;
foreach my $status_index (reverse 0 .. $#$statuses) {
my $s = $statuses->[$status_index];
my $tl_index = $self->_index($timeline, $s->{id});
my $existent = ($tl_index >= 0);
next if ($mode eq 'insert' && $existent) || ($mode eq 'update' && !$existent);
my $is_insert = ($mode eq 'insert');
if($mode eq 'upsert') {
$is_insert = (!$existent);
}
if($is_insert) {
unshift(@{$self->{timelines}{$timeline}}, dclone($s));
}else {
## update
$self->{timelines}{$timeline}[$tl_index] = dclone($s);
}
$put_count++;
}
if($put_count > 0) {
$self->{timelines}{$timeline} = sort_statuses($self->{timelines}{$timeline});
if(int(@{$self->{timelines}{$timeline}}) > $self->{max_status_num}) {
splice(@{$self->{timelines}{$timeline}}, -(int(@{$self->{timelines}{$timeline}}) - $self->{max_status_num}));
}
}
if($args{callback}) {
@_ = (undef, $put_count);
goto $args{callback};
}
}
sub delete_statuses {
my ($self, %args) = @_;
croak 'timeline arg is mandatory' if not defined $args{timeline};
croak 'ids arg is mandatory' if not exists $args{ids};
my $timeline = $args{timeline};
my $ids = $args{ids};
if(defined($ids)) {
if(!ref($ids)) {
$ids = [$ids];
}elsif(ref($ids) eq 'ARRAY') {
croak "ids arg array must not contain undef" if grep { !defined($_) } @$ids;
}else {
croak "ids must be undef/ID/ARRAYREF_OF_IDS";
}
}
if(!$self->{timelines}{$timeline}) {
if($args{callback}) {
@_ = (undef, 0);
goto $args{callback};
}
return;
}
my $delete_num = 0;
if(defined($ids)) {
foreach my $id (@$ids) {
my $tl_index = $self->_index($timeline, $id);
last if $tl_index < 0;
splice(@{$self->{timelines}{$timeline}}, $tl_index, 1);
$delete_num++;
( run in 0.954 second using v1.01-cache-2.11-cpan-39bf76dae61 )